昨天的 for 迴圈還很短,現在就換框架,看起來好像有點早。
所以今天不是要宣布手寫版本不行。我們用同一個任務、同一個模型、同一個 MCP Server,比較另一種表達流程的方法。LangGraph 在這個系列只安排一天,先把最有用的三個名詞弄懂就好。
State 是目前進度;Node 是要做的工作;Edge 是下一步要去哪裡。LangGraph 讓我們把這些東西明確分開,再編譯成可執行的圖。LangGraph Graph API
這裡的 State 放四項資料:訊息列表、工具觀察、模型呼叫次數、停止原因。沒有換掉 Day 2 的資料模型,也沒有把 Ollama 網址寫進節點。

圖裡有兩種方式到 END。結果到底是正常回答還是預算用完,仍要讀 stop_reason,不能只看流程已停止。
Model Node 呼叫原本的 llm.chat()。Tool Node 呼叫原本的 tools.call()。框架沒有多發明一套工具協定。
graph = StateGraph(State)
graph.add_node("model", model_node)
graph.add_node("tools", tool_node)
graph.add_edge(START, "model")
graph.add_conditional_edges("model", route, [END, "tools"])
模型結果帶著工具呼叫,就走 Tool Node;否則結束。工具執行後如果已達預算,也在這裡停止。所有邊都可以直接對照昨天的 if,不是另一套神祕的執行原理。
我們另外設定 graph recursion limit,作為控制流程的最後上限。但它計算的是圖的執行步驟,不等於工具呼叫次數,所以應用程式自己的計數不能刪掉。
"""Day 14:以 LangGraph 執行和 Day 13 相同的 MCP 工具任務。"""
import asyncio
from ironman.agent import print_result
from ironman.graph_agent import run_graph
from ironman.llm import OllamaClient
from ironman.mcp_bridge import devbench
async def main():
# MCP 和 LLM 的邊界不變;LangGraph 只負責狀態、節點與路由。
async with devbench() as tools, OllamaClient() as llm:
result = await run_graph(llm, tools)
print_result(result)
# 同時要有最終回答與真實 Observation,不只看圖是否停止。
assert result.stop_reason == "answered" and result.observations
if __name__ == "__main__":
asyncio.run(main())
今天的重點不是比較哪個版本快了幾毫秒。兩次推論可能受模型快取、GPU 負載與答案長度影響,單次結果不能代表效能結論。我們先確認兩個版本都能真的使用工具、回到模型,並產生包含來源的回答。
若只有「問模型、叫工具、回模型」這條小迴圈,普通 Python 很好讀。當你开始增加人工核准、分支、恢復執行或多個處理階段,圖能讓流程更容易討論與檢查。
不過,共享 State 也會帶來新的責任。哪些欄位會被覆寫、哪些需要累加,都要明確定義。今天用單一路徑執行,回傳新的訊息列表;若改成平行節點,不能假設兩份列表會自動依你想的順序合併。
框架整理的是流程,不是替結果背書,也不是新的安全邊界。工具權限依然由 Host 和 Server 控制。
LangGraph 就先到這裡。明天不再加框架功能,而是改變做事策略:如果任務本來就有好幾步,要不要先列計畫再動手?